package chaoxing_netdisk

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/chicken-team-outside/chicken_transmission/api_messages"
	"github.com/chicken-team-outside/chicken_transmission/driver"
	"github.com/chicken-team-outside/chicken_transmission/model"
	"github.com/chicken-team-outside/chicken_transmission/utils"
	"go.uber.org/zap"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

const FileListNum = 500

type ChaoXingNetDiskDriver struct {
	cookies []*http.Cookie
	token   string
	puid    string
}

func NewChaoXingNetDiskDriver(info *AuthInfo) (*ChaoXingNetDiskDriver, error) {
	dr := &ChaoXingNetDiskDriver{cookies: []*http.Cookie{
		{Name: "lv", Value: info.Lv, Path: "/"},
		{Name: "fid", Value: info.Fid, Path: "/"},
		{Name: "_uid", Value: info.Uid, Path: "/"},
		{Name: "uf", Value: info.Uf, Path: "/"},
		{Name: "_d", Value: info.D, Path: "/"},
		{Name: "UID", Value: info.UID, Path: "/"},
		{Name: "vc", Value: info.Vc, Path: "/"},
		{Name: "vc2", Value: info.Vc2, Path: "/"},
		{Name: "vc3", Value: info.Vc3, Path: "/"},
		{Name: "cx_p_token", Value: info.CxPToken, Path: "/"},
		{Name: "xxtenc", Value: info.Xxtenc, Path: "/"},
		{Name: "DSSTASH_LOG", Value: info.DsstashLog, Path: "/"},
	}, puid: info.Uid}
	var err error
	dr.token, err = dr.userValid(context.Background())
	if err != nil {
		return nil, err
	}
	return dr, nil
}

func (c *ChaoXingNetDiskDriver) UploadFile(ctx context.Context, size int64, destPath string, file io.ReadSeeker) error {
	fileName := filepath.Base(destPath)
	id, err := c.MkdirAllAndId(ctx, filepath.Dir(destPath))
	if err != nil {
		return err
	}
	return c.Upload(ctx, strconv.FormatInt(id, 10), fileName, size, file)
}

func (c *ChaoXingNetDiskDriver) GetFileDownloadLink(ctx context.Context, destPath string) (*api_messages.LinkInfo, error) {
	info, err := c.GetFileInfoByPath(ctx, destPath)
	if err != nil {
		return nil, err
	}
	cookie := strings.Builder{}
	for _, v := range c.cookies {
		cookie.WriteString(v.Name)
		cookie.WriteRune('=')
		cookie.WriteString(v.Value)
		cookie.WriteRune(';')
	}
	return &api_messages.LinkInfo{
		Link: c.Download(info.ResIdStr),
		Headers: map[string]string{
			"Referer": "https://pan-yz.chaoxing.com/",
			"Cookie":  cookie.String(),
		},
	}, nil
}

func (c *ChaoXingNetDiskDriver) MkdirAll(ctx context.Context, path string) error {
	_, err := c.MkdirAllAndId(ctx, path)
	return err
}

func (c *ChaoXingNetDiskDriver) MkdirAllAndId(ctx context.Context, path string) (int64, error) {
	path = filepath.ToSlash(path)
	parentId := int64(-1)
	paths := strings.Split(path, "/")
	err := c.dirWalker(ctx, func(currentId int64, info *FileInfo) (enter, stop bool) {
		if paths[0] == info.Name {
			parentId = info.ResId
			paths = paths[1:]
			if len(paths) == 0 {
				stop = true
			} else {
				enter = true
			}
		} else {
			parentId = currentId
		}
		return
	})
	if err != nil {
		return -1, err
	}
	if len(paths) > 0 {
		for _, name := range paths {
			fld, err := c.NewFld(ctx, name, strconv.FormatInt(parentId, 10))
			if err != nil {
				return -1, err
			}
			parentId = fld.Data.ResId
		}
	}
	return parentId, nil
}

func (c *ChaoXingNetDiskDriver) Exists(ctx context.Context, path string) (bool, error) {
	_, err := c.GetFileInfoByPath(ctx, path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func (c *ChaoXingNetDiskDriver) Remove(ctx context.Context, targetPath string) error {
	info, err := c.GetFileInfoByPath(ctx, targetPath)
	if err != nil {
		return err
	}
	rsp, err := c.Delete(ctx, info.ResIdStr)
	if err != nil {
		return err
	}
	if rsp.Result && rsp.Code == ResponseCodeSuccess {
		return nil
	} else {
		return rsp.Code
	}
}

func (c *ChaoXingNetDiskDriver) UserName(ctx context.Context) (string, error) {
	rsp, err := c.request(ctx, http.MethodGet, "https://i.mooc.chaoxing.com/space/index", nil)
	if err != nil {
		return "", err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != 200 {
		return "", StatusError(rsp.StatusCode)
	}
	body, err := io.ReadAll(rsp.Body)
	if err != nil {
		return "", err
	}
	return getResult(usernameRegexp, body), nil
}

func (c *ChaoXingNetDiskDriver) GetQuota(ctx context.Context) (used, total int64, err error) {
	capacity, err := c.GetUserDiskCapacity(ctx)
	if err != nil {
		return 0, 0, err
	}
	return capacity.DiskUsedCapacity, capacity.DiskTotalCapacity, nil
}

type BaseResponse struct {
	Code    ResponseCode `json:"code"`
	Msg     string       `json:"msg"`
	NewCode int          `json:"newCode"`
	Result  bool         `json:"result"`
}

func (b *BaseResponse) getError() error {
	if !b.Result && b.Code != ResponseCodeSuccess {
		return fmt.Errorf("error code %d: %s", b.Code, b.Msg)
	}
	return nil
}

func (c *ChaoXingNetDiskDriver) Close() error {
	return nil
}

func (c *ChaoXingNetDiskDriver) GetVendor() model.Vendor {
	return model.Vendor_ChaoXingNetDisk
}

func (c *ChaoXingNetDiskDriver) ListDir(ctx context.Context, path string) ([]*driver.FileInfo, error) {
	path = filepath.ToSlash(path)
	if path == "/" {
		return c.listAll(ctx, "")
	}
	info, err := c.GetFileInfoByPath(ctx, path)
	if err != nil {
		return nil, err
	}
	return c.listAll(ctx, info.ResIdStr)
}

func (c *ChaoXingNetDiskDriver) listAll(ctx context.Context, ptnId string) ([]*driver.FileInfo, error) {
	page := 1
	var result []*driver.FileInfo
	for {
		files, err := c.GetMyDirAndFiles(ctx, ptnId, page, FileListNum)
		if err != nil {
			return nil, err
		}
		for _, v := range files.Data {
			result = append(result, &driver.FileInfo{
				Name:  v.Name,
				Size:  v.Size,
				IsDir: !v.IsFile,
				Mtime: v.ModifyDate / 1000,
			})
		}
		if len(files.Data) < FileListNum {
			break
		} else {
			page += 1
		}
	}
	return result, nil
}

func (c *ChaoXingNetDiskDriver) dirWalker(ctx context.Context, walk func(currentId int64, info *FileInfo) (enter, stop bool)) error {
	var currentId string
	page := 1
	enter := false
	stop := false
	rootNode := true
	for {
		enter = false
		files, err := c.GetMyDirAndFiles(ctx, currentId, page, FileListNum)
		if err != nil {
			return err
		}
		if rootNode {
			rootNode = false
			enter, stop = walk(files.CurDir, &FileInfo{
				ResId:    files.CurDir,
				ResIdStr: strconv.FormatInt(files.CurDir, 10),
			})
			if stop || !enter {
				return nil
			}
		}
		for _, v := range files.Data {
			enter, stop = walk(files.CurDir, v)
			if stop {
				return nil
			}
			if enter {
				currentId = v.ResIdStr
				break
			}
		}
		if !enter {
			if len(files.Data) < FileListNum {
				break
			} else {
				page += 1
			}
		} else {
			page = 1
		}
	}
	return nil
}

/*
GetFileInfoByPath 获取指定路径文件信息

	path 文件绝对路径
*/
func (c *ChaoXingNetDiskDriver) GetFileInfoByPath(ctx context.Context, path string) (info *FileInfo, err error) {
	paths := strings.Split(filepath.ToSlash(path), "/")
	err = c.dirWalker(ctx, func(currentId int64, info_ *FileInfo) (enter, stop bool) {
		if paths[0] == info_.Name {
			if len(paths) == 1 {
				stop = true
				info = info_
			} else {
				paths = paths[1:]
				enter = true
			}
		}
		return
	})
	if err != nil {
		return nil, err
	}
	if info == nil {
		return nil, os.ErrNotExist
	}
	return
}

func (c *ChaoXingNetDiskDriver) requestNoRedirect(ctx context.Context, method, uri string, body io.Reader) (*http.Response, error) {
	return c.requestHelper(ctx, method, uri, body, false, "")
}

func (c *ChaoXingNetDiskDriver) requestCustomType(ctx context.Context, method, uri string, body io.Reader, contentType string) (*http.Response, error) {
	return c.requestHelper(ctx, method, uri, body, true, contentType)
}

func (c *ChaoXingNetDiskDriver) request(ctx context.Context, method, uri string, body io.Reader) (*http.Response, error) {
	return c.requestHelper(ctx, method, uri, body, true, "")
}

func (c *ChaoXingNetDiskDriver) requestHelper(ctx context.Context, method, uri string, body io.Reader, redirect bool, contentType string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, method, uri, body)
	if err != nil {
		return nil, err
	}
	for _, v := range c.cookies {
		req.AddCookie(v)
	}
	if !redirect {
		req.Header.Set(utils.RequestRedirectHeader, "0")
	}
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}
	req.Header.Set("User-Agent", userAgent)
	return utils.GetHttpClient().Do(req)
}

type UserToken struct {
	BaseResponse
	Token string `json:"_token"`
}

func (c *ChaoXingNetDiskDriver) userValid(ctx context.Context) (string, error) {
	rsp, err := c.request(ctx, http.MethodGet, "https://pan-yz.chaoxing.com/api/token/uservalid?", nil)
	if err != nil {
		return "", err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != 200 {
		return "", StatusError(rsp.StatusCode)
	}
	res := new(UserToken)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil {
		err = res.getError()
	}
	if err != nil {
		return "", err
	}
	return res.Token, nil
}

type CrcStatus struct {
	BaseResponse
	Exist bool `json:"exist"`
}

// CrcStatus 通过算法进行校验文件是否存在
func (c *ChaoXingNetDiskDriver) CrcStatus(ctx context.Context, crc string) (*CrcStatus, error) {
	form := url.Values{}
	form.Add("crc", crc)
	form.Add("_token", c.token)
	form.Add("puid", c.puid)
	rsp, err := c.request(ctx, http.MethodGet, "https://pan-yz.chaoxing.com/api/crcstatus?"+form.Encode(), nil)
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, StatusError(rsp.StatusCode)
	}
	res := new(CrcStatus)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil {
		err = res.getError()
	}
	if err != nil {
		return nil, err
	}
	return res, nil
}

type ResponseCode int

func (r ResponseCode) Error() string {
	return fmt.Sprintf("Status code: %d", r)
}

const (
	ResponseCodeSuccess = ResponseCode(2)
)

type UserDiskCapacityData struct {
	DiskUsedCapacity  int64 `json:"diskUsedCapacity"`
	DiskTotalCapacity int64 `json:"diskTotalCapacity"`
}

type userDiskCapacity struct {
	BaseResponse
	Data *UserDiskCapacityData `json:"data"`
}

// GetUserDiskCapacity 获取空间使用情况
func (c *ChaoXingNetDiskDriver) GetUserDiskCapacity(ctx context.Context) (*UserDiskCapacityData, error) {
	form := url.Values{}
	form.Add("_token", c.token)
	form.Add("puid", c.puid)
	rsp, err := c.request(ctx, http.MethodGet, "https://pan-yz.chaoxing.com/api/getUserDiskCapacity?"+form.Encode(), nil)
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, StatusError(rsp.StatusCode)
	}
	res := new(userDiskCapacity)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil {
		err = res.getError()
	}
	if err != nil {
		return nil, err
	}
	return res.Data, nil
}

type FileInfo struct {
	ResId        int64  `json:"resid"`
	Crc          string `json:"crc"`
	Puid         int    `json:"puid"`
	IsFile       bool   `json:"isfile"`
	PanType      string `json:"pantype"`
	Size         int64  `json:"size"`
	Name         string `json:"name"`
	ObjectId     string `json:"objectId"`
	ResType      string `json:"restype"`
	ModifyDate   int64  `json:"modifyDate"`
	ResIdStr     string `json:"residstr"`
	Suffix       string `json:"suffix"`
	Preview      string `json:"preview"`
	Thumbnail    string `json:"thumbnail"`
	Creator      int    `json:"creator"`
	Duration     int    `json:"duration"`
	IsImg        bool   `json:"isImg"`
	PreviewUrl   string `json:"previewUrl,omitempty"`
	Filetype     string `json:"filetype"`
	Filepath     string `json:"filepath"`
	ResTypeValue int    `json:"resTypeValue"`
	ExtInfo      string `json:"extinfo"`
}

type MyDirAndFilesResponse struct {
	BaseResponse
	Data       []*FileInfo `json:"data"`
	CurDir     int64       `json:"curDir"`
	ShareCount int         `json:"shareCount"`
}

/*
GetMyDirAndFiles 获取个人空间文件列表

	page 起始值为 1
	size 官方为500
	resIdStr 根目录为空(对应 FileInfo 的 ResIdStr 或 ResId 属性)
*/
func (c *ChaoXingNetDiskDriver) GetMyDirAndFiles(ctx context.Context, resIdStr string, page, size int) (*MyDirAndFilesResponse, error) {
	form := url.Values{}
	form.Add("puid", c.puid)
	form.Add("fldid", resIdStr)
	form.Add("orderby", "d")
	form.Add("order", "desc")
	form.Add("page", strconv.Itoa(page))
	form.Add("size", strconv.Itoa(size))
	form.Add("_token", c.token)
	form.Add("addrec", "false")
	form.Add("showCollect", "1")
	rsp, err := c.request(ctx, http.MethodGet, "https://pan-yz.chaoxing.com/api/getMyDirAndFiles?"+form.Encode(), nil)
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	res := new(MyDirAndFilesResponse)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil {
		err = res.getError()
	}
	if err != nil {
		return nil, err
	}
	return res, nil
}

type NewFldResponse struct {
	BaseResponse
	Data *FileInfo `json:"data"`
}

/*
NewFld 创建目录

	name 需要创建目录名
	pntId 父级目录 ResIdStr 值
*/
func (c *ChaoXingNetDiskDriver) NewFld(ctx context.Context, name, pntId string) (*NewFldResponse, error) {
	form := url.Values{}
	form.Add("puid", c.puid)
	form.Add("_token", c.token)
	form.Add("name", name)
	form.Add("pntid", pntId)
	rsp, err := c.request(ctx, http.MethodGet, "https://pan-yz.chaoxing.com/api/newfld?"+form.Encode(), nil)
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, StatusError(rsp.StatusCode)
	}
	res := new(NewFldResponse)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil {
		err = res.getError()
	}
	if err != nil {
		return nil, err
	}
	return res, nil
}

type DeleteResult struct {
	Resid   int64 `json:"resid"`
	Success bool  `json:"success"`
}

type DeleteResponse struct {
	BaseResponse
	Data []*DeleteResult `json:"data"`
}

/*
Delete 删除目录或文件

	resIds 需要删除的目录或文件 ResId 列表
*/
func (c *ChaoXingNetDiskDriver) Delete(ctx context.Context, resIds ...string) (*DeleteResponse, error) {
	form := url.Values{}
	form.Add("puid", c.puid)
	form.Add("_token", c.token)
	form.Add("resids", strings.Join(resIds, ","))
	rsp, err := c.request(ctx, http.MethodPost, "https://pan-yz.chaoxing.com/api/delete", strings.NewReader(form.Encode()))
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	res := new(DeleteResponse)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil {
		err = res.getError()
	}
	if err != nil {
		return nil, err
	}
	return res, nil
}

/*
Download 获取下载链接

	resId 需要下載的文件 ResId
*/
func (c *ChaoXingNetDiskDriver) Download(resId string) string {
	form := url.Values{}
	form.Add("puid", c.puid)
	form.Add("fleid", resId)
	return "https://pan-yz.chaoxing.com/download/downloadfile?" + form.Encode()
}

const BytesPerChunk = 512 * 1024
const LIMIT = 1024 * 1024

type CreateFileNewResponse struct {
	BaseResponse
	Crc     string    `json:"crc"`
	TimeMil string    `json:"timemil"`
	Data    *FileInfo `json:"data"`
}

/*
createFileNew 秒传上传文件

	有且仅有文件大于等于 LIMIT + BytesPerChunk*2 时才需要调用
	folderId 父目录 ResIdStr
	fileName 文件名
	fileSize 文件大小
*/
func (c *ChaoXingNetDiskDriver) createFileNew(ctx context.Context, folderId string, fileName string, fileSize int64, file io.ReadSeeker) (*CreateFileNewResponse, error) {
	mCtx, cancel := context.WithCancelCause(ctx)
	form, formType := utils.StreamForm(func(writer *multipart.Writer) {
		err := writer.WriteField("size", strconv.FormatInt(fileSize, 10))
		if err == nil {
			err = writer.WriteField("fn", fileName)
		}
		var w io.Writer
		if err == nil {
			w, err = writer.CreateFormFile("file0", "blob")
		}
		warp := utils.NewReadAtWarp(file)
		if err == nil {
			_, err = io.Copy(w, io.NewSectionReader(warp, 0, BytesPerChunk))
		}
		if err == nil {
			w, err = writer.CreateFormFile("file1", "blob")
		}
		if err == nil {
			_, err = io.Copy(w, io.NewSectionReader(warp, BytesPerChunk+fileSize-LIMIT, BytesPerChunk))
		}
		if err == nil {
			err = writer.WriteField("fldid", folderId)
		}
		if err == nil {
			err = writer.WriteField("puid", c.puid)
		}
		if err != nil {
			cancel(err)
		}
	})
	defer form.Close()
	rsp, err := c.requestCustomType(mCtx, http.MethodPost, "https://pan-yz.chaoxing.com/opt/createfilenew", form, formType)
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, StatusError(rsp.StatusCode)
	}
	res := new(CreateFileNewResponse)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil && res.Crc == "" {
		err = res.getError()
	}
	if err != nil {
		return nil, err
	}
	return res, nil
}

type UploadResponse struct {
	Success bool   `json:"success"`
	Msg     string `json:"msg"`
}

var ErrUploadLimit = errors.New("upload size limit exceeded")

/*
Upload 上传文件

	folderId 父目录 ResIdStr
	fileName 文件名
	fileSize 文件大小
	file 文件数据
*/
func (c *ChaoXingNetDiskDriver) Upload(ctx context.Context, folderId string, fileName string, fileSize int64, file io.ReadSeeker) (err error) {
	//减少不必要的检查
	//limit, err := c.GetLimitFlow(ctx)
	//if err != nil {
	//	return err
	//}
	//if fileSize >= limit {
	//	return ErrUploadLimit
	//}
	var fileNew *CreateFileNewResponse
	if fileSize >= LIMIT+BytesPerChunk*2 {
		fileNew, err = c.createFileNew(ctx, folderId, fileName, fileSize, file)
		if err != nil {
			return err
		}
		if fileNew.Result {
			return nil
		}
	}
	mCtx, cancel := context.WithCancelCause(ctx)
	form, formType := utils.StreamForm(func(writer *multipart.Writer) {
		fileType := mime.TypeByExtension(filepath.Ext(fileName))
		if fileType == "" {
			fileType = "application/octet-stream"
		}
		err = writer.WriteField("folderId", folderId)
		if err == nil {
			err = writer.WriteField("puid", "0")
		}
		if err == nil {
			err = writer.WriteField("id", "WU_FILE_0")
		}
		if err == nil {
			err = writer.WriteField("name", fileName)
		}
		if err == nil {
			err = writer.WriteField("type", fileType)
		}
		if err == nil {
			err = writer.WriteField("lastModifiedDate", time.Now().Format(time.RFC3339))
		}
		if err == nil {
			err = writer.WriteField("size", strconv.FormatInt(fileSize, 10))
		}
		if fileNew != nil {
			err = writer.WriteField("ts", fileNew.TimeMil)
			if err == nil {
				err = writer.WriteField("crc", fileNew.Crc)
			}
		}
		if err == nil {
			var w io.Writer
			w, err = writer.CreateFormFile("file", fileName)
			if err == nil {
				_, err = file.Seek(0, io.SeekStart)
			}
			if err == nil {
				_, err = io.Copy(w, file)
			}
		}
		if err != nil {
			cancel(err)
		}
	})
	defer form.Close()
	rsp, err := c.requestCustomType(mCtx, http.MethodPost, "https://pan-yz.chaoxing.com/opt/upload", form, formType)
	if err != nil {
		return err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		all, _ := io.ReadAll(rsp.Body)
		zap.L().Debug("invalid response code", zap.ByteString("body", all), zap.Int("code", rsp.StatusCode))
		return StatusError(rsp.StatusCode)
	}
	res := new(UploadResponse)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err == nil {
		if !res.Success {
			switch res.Msg {
			case "文件名含敏感词":
				return driver.ErrPolicyRejection
			case "文件名含特殊字符":
				return driver.ErrInvalidFileName
			case "云盘空间不足":
				return driver.ErrExceedSpaceQuota
			default:
				return errors.New(res.Msg)
			}
		}
	}
	return err
}

type LimitFlowResponse struct {
	UploadFlag bool  `json:"uploadFlag"`
	Filesize   int64 `json:"filesize"`
}

// GetLimitFlow 获取上传大小限制
func (c *ChaoXingNetDiskDriver) GetLimitFlow(ctx context.Context) (int64, error) {
	rsp, err := c.request(ctx, http.MethodGet, "https://pan-yz.chaoxing.com/opt/getLimitFlow", nil)
	if err != nil {
		return 0, err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return 0, StatusError(rsp.StatusCode)
	}
	res := new(LimitFlowResponse)
	err = json.NewDecoder(rsp.Body).Decode(res)
	if err != nil {
		return 0, err
	}
	return res.Filesize, nil
}
